import querystring from 'querystring';
import yargs from 'yargs/yargs';
import coBody from 'co-body';
import { IncomingForm } from 'formidable';
import { WebContextHelper } from '../core/WebContext';
import { ConsoleContextHelper } from '../core/ConsoleContext';

export type Next = () => Promise<any>;

export type WebPlugin<Props = {}, State = {}> = Plugin<Plugin.Web, Props, State> | Plugin<Plugin.All, Props, State>;
export type ConsolePlugin<Props = {}, State = {}> = Plugin<Plugin.Console, Props, State> | Plugin<Plugin.All, Props, State>;
export type AllPlugin<Props = {}, State = {}> = WebPlugin<Props, State> | ConsolePlugin<Props, State>;

export type CompatiblePlugin<Props = {}, State = {}> = WebMiddleware<Props, State> | ConsoleMiddleware<Props, State> | WebPlugin<Props, State> | ConsolePlugin<Props, State>;

export type Middleware<Props = {}, State = {}> = (context: WebContextHelper<Props, State> | ConsoleContextHelper<Props, State>, next: Next) => any;
export type WebMiddleware<Props = {}, State = {}> = (context: WebContextHelper<Props, State>, next: Next) => any;
export type ConsoleMiddleware<Props = {}, State = {}> = (context: ConsoleContextHelper<Props, State>, next: Next) => any;

type PluginContext<Type, Props, State> = Type extends Plugin.All
  ? Middleware<Props, State>
  : Type extends Plugin.Console
    ? ConsoleMiddleware<Props, State>
    : WebMiddleware<Props, State>;

export namespace Plugin {
  export type Web = 'web';
  export type Console = 'console';
  export type All = 'all';
}

export abstract class Plugin<Type extends Plugin.All | Plugin.Web | Plugin.Console = Plugin.All, Props = {}, State = {}> {
  private middleware: PluginContext<Type, Props, State>[] = [];

  public/*protected*/ collect(): PluginContext<Type, Props, State>[] {
    return this.middleware;
  }

  protected use(fn: PluginContext<Type, Props, State>): {
    use: Plugin<Type, Props, State>['use'];
  };
  protected use<P, S>(
    plugin: Type extends Plugin.Web
      ? WebPlugin<P, S>
      : Type extends Plugin.Console
        ? ConsolePlugin<P, S>
        : AllPlugin<P, S>
  ): {
    use: Plugin<Type, P & Props, S & State>['use'];
  };
  // @ts-expect-error to compatible with koa.Middleware
  protected use(fn: (ctx: any, next: Next) => any): {
    use: Plugin<Type, Props, State>['use'];
  };
  protected use(fn: PluginContext<Type, Props, State> | AllPlugin<any, any>): object {
    if (typeof fn === 'function') {
      this.middleware.push(fn);
    } else{
      this.middleware.push(...fn.collect() as any[]);
    }

    return this;
  }

  /**
   * You expect user have mounted plugin to router before this plugin,
   * so that you can safely use external props and states, with related type declaration on context.
   *
   * But remember that program won't actually check expected plugin,
   * you should tell user when to mount it and append to `package.json` like this:
   *
   * {
   *   "peerDependencies": {
   *     "expected-pkg": "^x.y.z"
   *   },
   *   "dependencies": {},
   *   "devDependencies": {
   *     "expected-pkg": "^x.y.z"
   *   }
   * }
   */
  protected expect<P, S>(
    // @ts-expect-error
    CustomPlugin: Type extends Plugin.Web
      ? new(...args: any[]) => WebPlugin<P, S>
      : Type extends Plugin.Console
        ? new(...args: any[]) => ConsolePlugin<P, S>
        : new(...args: any[]) => AllPlugin<P, S>
  ): {
    use: Plugin<Type, P & Props, S & State>['use'];
    expect: Plugin<Type, P & Props, S & State>['expect'];
  } {
    // @ts-expect-error
    return this;
  }

  protected getQuery(ctx: WebContextHelper): querystring.ParsedUrlQuery {
    return querystring.parse(ctx.request.querystring);
  }

  protected async getBody(ctx: WebContextHelper): Promise<Record<string, any>> {
    try {
      if (ctx.request.is('multipart/*')) {
        const form = new IncomingForm({
          multiples: true,
          hash: false,
        });

        return new Promise((resolve, reject) => {
          form.parse(ctx.request.req, (err, fields, files) => {
            if (err) {
              return reject(err);
            }

            resolve({
              ...fields,
              ...files,
            });
          });
        });
      }

      return coBody(ctx.request.req, {
        returnRawBody: false,
      });
    } catch (e) {
      return ctx.throw(400, e);
    }
  }

  protected getParams(ctx: WebContextHelper): Record<string, string> {
    return ctx.request.rawParams;
  }

  protected getOptions(ctx: ConsoleContextHelper, aliases?: Record<string, string | string[] | undefined>): Record<string, any> {
    const input = yargs([]).help(false).version(false);

    if (aliases) {
      Object.entries(aliases).forEach(([name, alias]) => {
        input.options(name, {
          alias,
        });
      });
    }

    const { _, $0, ...options } = input.parse(ctx.argv);

    return options;
  }
}
